import { readFile } from 'fs/promises'
import path from 'path'
import { PluginContext, PluginHooks } from 'rollup'
//@ts-ignore
import { ResolvedConfig, ServerHook } from 'vite'
import { CWD } from '../constants'
import utilsModule from './modules/utils'
import { generageImports, VirtualModuleInfo } from './modules/_utils'
import { defineHelper, getVirtualModules } from './_utils'

export const getHelper = defineHelper((options, registerVirtualModule) => {
  const virtualModules = getVirtualModules(options)

  const generateBundle: PluginHooks['generateBundle'] = function () {
    const ownKeys = Reflect.ownKeys(virtualModules)
    for (const key of ownKeys) {
      if (typeof key === 'string') delete virtualModules[key]
    }
  }

  const resolveId: PluginHooks['resolveId'] = function (id) {
    if (id in virtualModules) return id
  }

  const load: PluginHooks['load'] = function (id) {
    return virtualModules[id]
  }

  const getInlineBufferModule = (buffer: Buffer): VirtualModuleInfo<'default'> => {
    let code: string
    if (buffer.length < 128) {
      code = `export default new Uint8Array(${JSON.stringify(Array.from(buffer))});`
    } else {
      code =
        generageImports([utilsModule, ['stringToUInt8Array']]) +
        `export default stringToUInt8Array(${JSON.stringify(buffer.toString('base64'))}, 'base64');`
    }

    return {
      id: registerVirtualModule(code),
      code,
      exports: ['default'],
    }
  }

  const getReadFileBufferModule = async <A extends boolean | 'both'>(
    context: PluginContext,
    fileName: string,
    source?: Buffer,
    _async?: A
  ): Promise<
    A extends 'both' ? VirtualModuleInfo<'sync_read' | 'async_read'> : VirtualModuleInfo<'default'>
  > => {
    let code
    const stringifyUrl = await fileToStringifyUrl(context, fileName, source)
    if (_async === 'both') {
      code =
        generageImports([utilsModule, ['readFile', 'readFileSync']]) +
        `const fileName = ${stringifyUrl};` +
        `let result;` +
        `export const sync_read = () => (result ?? (result = readFileSync(fileName)));` +
        `export const async_read = async () => (result ?? (result = await readFile(fileName)));`
      return {
        id: registerVirtualModule(code),
        code,
        exports: ['asyc_read', 'async_read'],
      } as any
    } else if (_async) {
      code =
        generageImports([utilsModule, ['readFile']]) +
        `const fileName = ${stringifyUrl};` +
        `let result;` +
        `export default async () => (result ?? (result = await readFile(fileName)));`
    } else {
      code =
        generageImports([utilsModule, ['readFileSync']]) +
        `const fileName = ${stringifyUrl};` +
        `let result;` +
        `export default () => (result ?? (result = readFileSync(fileName)));`
    }

    return {
      id: registerVirtualModule(code),
      code,
      exports: ['default'],
    } as any
  }

  let fileToStringifyUrl = async (context: PluginContext, fileName: string, source?: Buffer) => {
    return `import.meta.ROLLUP_FILE_URL_${context.emitFile({
      type: 'asset',
      name: path.relative(CWD, path.basename(fileName)).replace('^.+?[:/\\]', ''),
      source: source ?? (await readFile(fileName)),
    })}`
  }

  let addWatchFile = (context: PluginContext, id: string, ...fileNames: string[]) => {
    for (const _fileName of fileNames) {
      context.addWatchFile(_fileName)
    }
  }

  const configResolved = (config: ResolvedConfig) => {
    let { publicDir } = config
    if (publicDir) {
      publicDir = path.normalize(path.resolve(CWD, publicDir))
    }
    const checkPublicFile = (url: string): string | undefined => {
      // note if the file is in /public, the resolver would have returned it
      // as-is so it's not going to be a fully resolved path.
      if (!publicDir) {
        return
      }
      url = path.normalize(path.resolve(CWD, url))

      if (url.startsWith(publicDir)) {
        return url
      }
    }

    if (config.command === 'serve') {
      const FS_PREFIX = `/@fs/`

      fileToStringifyUrl = returnObject.fileToStringifyUrl = async (context, fileName) => {
        let rtn: string
        if (checkPublicFile(fileName)) {
          // in public dir, keep the url as-is
          rtn = fileName
        } else if (fileName.startsWith(config.root)) {
          // in project root, infer short public path
          rtn = '/' + path.posix.relative(config.root, fileName)
        } else {
          // outside of project root, use absolute fs path
          // (this is special handled by the serve static middleware
          rtn = path.posix.join(FS_PREFIX + fileName)
        }
        rtn = rtn
        const origin = config.server?.origin ?? ''
        return JSON.stringify(origin + config.base + rtn.replace(/^\//, ''))
      }
    } 
    // else {
    //   fileToStringifyUrl = returnObject.fileToStringifyUrl = async (context, fileName, source) => {
    //     return `__VITE_ASSET__${context.emitFile({
    //       name: path.relative(CWD, path.basename(fileName)).replace('^.+?[:/\\]', ''),
    //       type: 'asset',
    //       source: source ?? (await readFile(fileName)),
    //     })}__`
    //   }
    // }
  }

  const configureServer: ServerHook = (server) => {
    const { moduleGraph, watcher } = server
    addWatchFile = returnObject.addWatchFile = (context, id, ...fileNames) => {
      fileNames = fileNames
        .map((fileName) => {
          return path.normalize(path.resolve(CWD, fileName))
        })
        .filter(Boolean)

      for (const _fileName of fileNames) {
        watcher.add(_fileName)
      }

      const handle = (event: any, fileName: string) => {
        fileName = path.normalize(path.resolve(CWD, fileName))
        if (fileNames.includes(fileName)) {
          watcher.off('all', handle)
          const mod = moduleGraph.getModuleById(id)
          if (mod) {
            moduleGraph.invalidateModule(mod)
          }
        }
      }

      watcher.on('all', handle)
    }

    options.inline = () => true
  }

  const returnObject = {
    getInlineBufferModule,
    getReadFileBufferModule,
    fileToStringifyUrl,
    addWatchFile,
    configResolved,
    configureServer,
    generateBundle,
    resolveId,
    load,
  }

  return returnObject
})
